package gapidt.analysis.detectors;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.lang.ArrayUtils;
import org.apache.log4j.Logger;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.jgrapht.UndirectedGraph;
import org.jgrapht.alg.ConnectivityInspector;
import org.jgrapht.graph.DefaultEdge;
import org.jgrapht.graph.SimpleGraph;

import edu.stanford.nlp.util.StringUtils;
import gapidt.analysis.APIDesignAnalysisUtils;
import gapidt.analysis.ASTUtils;
import gapidt.analysis.DesignProblem;
import gapidt.analysis.IDesignProblemsDetector;
import gapidt.analysis.designproblems.LowCohesiveMethod;
import gapidt.nlp.SemanticUtils;
import gapidt.plugin.AnalysisResultsManager;

public class LowCohesiveMethodsDetector extends IDesignProblemsDetector<IType>{
	
	private static Logger logger = Logger.getLogger(LowCohesiveMethodsDetector.class);
	
	private static String[] primitveTypes = {
		"java.lang.Byte", "java.lang.Short", "java.lang.Integer", "java.lang.Long",
		"java.lang.Float", "java.lang.Double", "java.lang.Boolean",
		"java.lang.Character", "java.lang.Class", "java.lang.Object", 
		"java.lang.String", 
	};
	
	private static Map<String, String> primitiveTypesNames;
	static{
		primitiveTypesNames = new HashMap<String, String>();
		primitiveTypesNames.put("java.lang.Byte", "byte");
		primitiveTypesNames.put("java.lang.Short", "short");
		primitiveTypesNames.put("java.lang.Integer", "int");
		primitiveTypesNames.put("java.lang.Long", "long");
		primitiveTypesNames.put("java.lang.Float", "float");
		primitiveTypesNames.put("java.lang.Double", "double");
		primitiveTypesNames.put("java.lang.Boolean", "boolean");
		primitiveTypesNames.put("java.lang.Character", "char");
	}
	
	private static String[] ignoredPackages = {
		"java.lang", "java.util"
	};
	
	public LowCohesiveMethodsDetector(){
		super(IType.class);
	}
	
	@Override
	public List<DesignProblem<?>> detectProblems(IType type){
		List<DesignProblem<?>> designProblems = new ArrayList<DesignProblem<?>>();

		TypeDeclaration typeDeclaration = ASTUtils.parse(type);
		
		if(typeDeclaration == null) return designProblems;
		
		UndirectedGraph<String, DefaultEdge> graph = new SimpleGraph<String, DefaultEdge>(DefaultEdge.class);
		
		try{
			
			//Crear nodos y vertices en base a la clase a analizar
			addTypeDeclaration(typeDeclaration, graph);
			
			ConnectivityInspector<String, DefaultEdge> ci = new ConnectivityInspector<String, DefaultEdge>(graph);
			if(!ci.isGraphConnected()){
				logger.debug("Type '"+typeDeclaration.getName()+"' graph is not connected");
				
				List<MethodDeclaration> addedMethods = new ArrayList<MethodDeclaration>();
				MethodDeclaration[] methods = typeDeclaration.getMethods();
				for(int i = 0; i < methods.length; i++)
					if(graph.containsVertex(getNodeName(methods[i], "")))
						addedMethods.add(methods[i]);
				
				if(addedMethods.size() > 1){
					//Obtener el subgrafo conectado más grande
					List<Set<String>> connectedSets = ci.connectedSets();
					Set<String> set = connectedSets.get(0);
					
					int i = 1;
					int biggerSetSize = countSetNodes(set, "m"); // Obtener cantidad de métodos en el set de nodos
					for(Set<String> s : connectedSets){
						int setSize = countSetNodes(s, "m");
						logger.debug("Set "+i+" size is "+setSize);
						if(setSize > biggerSetSize){
							set = s;
							biggerSetSize = setSize;
						}
						i++;
					}
					logger.debug("Bigger connected set size is "+biggerSetSize);
					
					List<String> cohesiveMethodsNames = new ArrayList<String>();
					for(String methodNodeName : set)
						for(MethodDeclaration method : addedMethods)
							if(getNodeName(method, "").equals(methodNodeName)){
								cohesiveMethodsNames.add(method.getName().toString());
								break;
							}
						
					//Los métodos fuera del primer subgrafo conectado no son cohesivos
					for(MethodDeclaration method : addedMethods){
						if(!set.contains(getNodeName(method, ""))){
							//Buscar IMethod correspondiente
							for(IMethod iMethod : type.getMethods()){
								if(ASTUtils.areEqual(iMethod, method)){
									designProblems.add(new LowCohesiveMethod(iMethod, StringUtils.join(cohesiveMethodsNames, ", ")));
									AnalysisResultsManager.getInstance().addDesigProblemCount(getClass().getSimpleName());
									logger.debug("Method '"+method.getName()+"' of type '"+typeDeclaration.getName()+"' is not connected to main subgraph including the method '"+cohesiveMethodsNames.get(0)+"'");
								}
							}
						}
					}
				}else{
					logger.debug("Type '"+typeDeclaration.getName()+"' has less than 1 method. Skipping analysis");
				}
			}else{
				logger.debug("Type '"+typeDeclaration.getName()+"' graph is connected");
			}

		}catch(Exception e){
			logger.error("Exception occurred while detecting non-cohesive methods in type '"+typeDeclaration.getName()+"'", e);
		}
		
		return designProblems;
	}
	
	private void addTypeDeclaration(TypeDeclaration typeDeclaration, UndirectedGraph<String, DefaultEdge> graph){
		ITypeBinding typeBinding = typeDeclaration.resolveBinding();
		
		//Agregar un nodo por cada método pero no conectarlo con el nodo de la clase
		for(MethodDeclaration method : typeDeclaration.getMethods())
			addMethod(method, graph, typeBinding);
	}
	
	/**
	 * Agregar un nodo al grafo por cada atributo y método de la clase dada (type).
	 * Si connectWithVertices es verdadero se agregan vértices conectando
	 * la clase con cada método y atributo.
	 * 
	 * @param type
	 * @param graph
	 * @param connectWithVertices
	 */
	private boolean addType(ITypeBinding type, String fieldName, UndirectedGraph<String, DefaultEdge> graph, ITypeBinding analyzedType){
		
		String typeNodeName = getNodeName(type, fieldName);
		boolean shouldIgnoreType = shouldIgnoreType(type);

		if(shouldIgnoreType && (fieldName == null || fieldName.isEmpty())){
			logger.debug("Ignore type "+type.getQualifiedName());
			return false;
		}

		//Salir si la clase ya fue agregada al grafo
		if(graph.containsVertex(typeNodeName)){
			logger.debug("Type already exists: "+typeNodeName);
			return true; 
		}

		//No conectar el tipo analizado con sus métodos y atributos
		boolean connectWithVertices = !typeNodeName.equals(getNodeName(analyzedType, ""));

		//Agregar nodo de la clase
		graph.addVertex(typeNodeName);
		logger.debug("Added type node: "+typeNodeName);
		
		if(!shouldIgnoreType){
			//Agregar un nodo por cada atributo
			for(IVariableBinding field : type.getDeclaredFields()){
				//Obtener el type binding del atributo
				ITypeBinding fieldTypeBinding = field.getType();
				if(fieldTypeBinding != null){
					boolean added = addType(fieldTypeBinding, field.getName(), graph, analyzedType);
					if(added && connectWithVertices){
						String fieldTypeName = getNodeName(fieldTypeBinding, field.getName());
						graph.addEdge(typeNodeName, fieldTypeName);
						logger.debug("Added type-attribute connection: "+typeNodeName+" <-> "+fieldTypeName);
					}
				}
			}
		}
		
		//Si es un tipo parametrizado, agregar un nodo por cada tipo parámetro
		if(type.isParameterizedType() && type.getTypeArguments() != null){
			for(ITypeBinding typeArgument : type.getTypeArguments()){
				boolean added = addType(typeArgument, "", graph, analyzedType);
				if(added){
					String typeArgumentName = getNodeName(typeArgument, "");
					graph.addEdge(typeNodeName, typeArgumentName);
					logger.debug("Added type - type parameter connection: "+typeNodeName+" <-> "+typeArgumentName);
				}
			}
		}
		
		return true;
	}
	
	/**
	 * Agregar un nodo al grafo por cada parámetro y otro por el tipo de retorno del método.
	 * Si connectWithVertices es verdadero se agregan vértices conectando 
	 * el método con cada parámetro y con el tipo de retorno.
	 * 
	 * @param type
	 * @param graph
	 * @param connectWithVertices
	 */
	private boolean addMethod(MethodDeclaration methodDeclaration, UndirectedGraph<String, DefaultEdge> graph, ITypeBinding analyzedType){
		
		IMethodBinding method = methodDeclaration.resolveBinding();
		if(method == null){
			logger.debug("Can't resolve binding for method '"+methodDeclaration.getName().toString()+"'");
			return false;
		}
		String methodNodeName = getNodeName(method, "");
		
		if(!APIDesignAnalysisUtils.isServiceMethod(((IMethod) method.getJavaElement())))
			return false;
		
		//Salir si el método ya fue agregado al grafo
		if(graph.containsVertex(methodNodeName)) return true;
		
		//Agregar nodo del método
		graph.addVertex(methodNodeName);
		
		//Agregar un nodo por cada parámetro
		for(Object p : methodDeclaration.parameters()){
			SingleVariableDeclaration parameter = (SingleVariableDeclaration) p;
			ITypeBinding parameterTypeBinding = parameter.getType().resolveBinding();
			boolean added = addType(parameterTypeBinding, parameter.getName().toString(), graph, analyzedType);
			if(added){
				String parameterTypeName = getNodeName(parameterTypeBinding, parameter.getName().toString());
				graph.addEdge(methodNodeName, parameterTypeName);
				logger.debug("Added method-parameter connection: "+methodNodeName+" <-> "+parameterTypeName);
			}
		}
		
		//Agregar un nodo por el tipo de retorno si existe
		ITypeBinding returnTypeBinding = method.getReturnType();
		if(returnTypeBinding != null){
			boolean added = addType(returnTypeBinding, "", graph, analyzedType);
			if(added){
				String returnTypeName = getNodeName(returnTypeBinding, "");
				graph.addEdge(methodNodeName, returnTypeName);
				logger.debug("Added method-return type connection: "+methodNodeName+" <-> "+returnTypeName);
			}
		}
		
		//Agregar un nodo con la frase sustantiva del método
		List<String> nonVerbs = SemanticUtils.getNonVerbs(SemanticUtils.camelCase2Human(method.getName())); 
		if(nonVerbs.size() > 0){
			for(String w : nonVerbs){
				String methodNounPhraseNodeName = "n:"+w.toLowerCase();
				if(!graph.containsVertex(methodNounPhraseNodeName)){
					graph.addVertex(methodNounPhraseNodeName);
				}
				graph.addEdge(methodNodeName, methodNounPhraseNodeName);
				logger.debug("Added method-noun phrase connection: "+methodNodeName+" <-> "+methodNounPhraseNodeName);
			}
		}
		
		logger.debug("Added method node: "+methodNodeName);
		
		return true;
	}
	
	/**
	 * Dado un objeto devuelve el nombre del nodo que lo representa en el grafo
	 * 
	 * @param o
	 * @return
	 */
	private String getNodeName(Object o, String name){
		String nodeName = "";
		if(o != null){
			if(o instanceof MethodDeclaration){
				MethodDeclaration methodDeclaration = (MethodDeclaration) o;
				IMethodBinding m = methodDeclaration.resolveBinding();
				List<String> params = new ArrayList<String>();
				for(Object p : methodDeclaration.parameters()){
					SingleVariableDeclaration parameter = (SingleVariableDeclaration) p;
					params.add(getNodeName(parameter.getType().resolveBinding(), parameter.getName().toString()));
				}
				nodeName = "m:"+(m.getDeclaringClass()!=null?m.getDeclaringClass().getQualifiedName()+".":"")+m.getName()+(params.size()>0?"-"+StringUtils.join(params, ";"):"");
			}else if(o instanceof IMethodBinding){
				MethodDeclaration m = ASTUtils.parse((IMethod) ((IMethodBinding) o).getJavaElement());
				nodeName = getNodeName(m, "");
			}else if(o instanceof ITypeBinding){
				ITypeBinding t = (ITypeBinding) o;
				String typeName = t.getQualifiedName().replaceAll("\\[[0-9]*\\]", "");
				
				// Normalize type name for certain primitive types
				if(primitiveTypesNames.containsKey(typeName))
					typeName = primitiveTypesNames.get(typeName);
				
				nodeName = "t:"+ typeName;
				
				if(name != null && !name.isEmpty() && isPrimitiveType(t)){
					nodeName += ":" + normalizeTypeName(name);
				}
			}else{
				nodeName = o.toString();
			}
		}else{
			nodeName = "o:null";
		}
		return nodeName;
	}
	
	protected boolean shouldIgnoreType(ITypeBinding type){
		if(type.isParameterizedType()){
			// Un tipo parametrizado debe ignorarse si todos sus tipos parámetros deben ignorarse
			if(type.getTypeArguments() != null && type.getTypeArguments().length > 0){
				for(ITypeBinding typeArgument : type.getTypeArguments()){
					if(!shouldIgnoreType(typeArgument))
						return false;
				}
			}else{
				return true;
			}
		}
		
		String qualifiedTypeName = type.getQualifiedName();
		if(qualifiedTypeName != null && !qualifiedTypeName.isEmpty()){
			for(String pkg : ignoredPackages){
				if(qualifiedTypeName.startsWith(pkg))
					return true;
			}
		}
		
		return isPrimitiveType(type);
	}
	
	protected boolean isPrimitiveType(ITypeBinding type){
		if(type != null){
			String qualifiedName = type.getQualifiedName().replaceAll("\\[[0-9]*\\]", "");
			return (type.isPrimitive() || ArrayUtils.contains(primitveTypes, qualifiedName));
		}
		return false;
	}
	
	protected String normalizeTypeName(String name){
		if(name == null || name.isEmpty()) return "";
		return SemanticUtils.camelCase2Underscores(name).toLowerCase();
	}
	
	protected int countSetNodes(Set<String> set, String type){
		int count = 0;
		for(String n : set){
			if(n.startsWith(type + ":")){
				count++;
			}
		}
		return count;
	}
}
